package org.rowan.adi.camera;

import org.rowan.adi.Vector;
import org.rowan.adi.camera.Rotation;

/**
 * A fully controllable 3-dimensional camera. A Camera can be manipulated relative
 * to its current location/orientation, or its location/orientation can be set
 * directly.
 * <p>
 * A Camera's orientation is maintained as a rotation relative to a default
 * orientation, in which the camera is "looking" down the world -z axis with its 
 * local y axis relative to the world y axis.
 * <p>
 * Cameras maintain a focus depth that can be set and altered by the user. All 
 * Cameras are initialized with a default focus depth of 1.0. 
 * <p>
 * At any time, the user can lock a Camera's focus. This causes all subsequent 
 * movement to be orbital, around the current focus point. By default all Camera's 
 * are created with focus unlocked.
 * <p>
 * By default, Cameras are initialized with field of view set to 35 degrees, near 
 * clipping distance set to 0.01, and far clipping distance set to 
 * <code>Double.MAX_VALUE</code>. However, these values can be set manually by 
 * the user.
 *
 * @author Spence DiNicolantonio
 * @version 1.0
 */
public class Camera {
	
	/*********************************************
	 * MARK: Constants
	 *********************************************/
	
	public static final double MIN_FOCUS_DEPTH = 0.001;
	
	
	/*********************************************
	 * MARK: Fields
	 *********************************************/
	
	/* The current location of this Camera */
	private Vector location;
	
	/* The current orientation of this Camera relative to the default orientation. */
	private Rotation orientation;
	
	/* The distance between this Camera and the point of focus. */
	private double focusDepth;
	
	/* Flag defining whether this Camera's focus is locked. */
	private boolean focusLocked;
	
	/* This Camera's field of view angle, in radians. */
	private double fieldOfView;
	
	/* The distance between this Camera and the near clipping plane. */
	private double nearClip;
	
	/* The distance between this Camera and the far clipping plane. */
	private double farClip;
	
	
	/*********************************************
	 * MARK: Constructors
	 *********************************************/	
		
	/**
	 * Designated Constructor.
	 * Creates a Camera positioned at a given location with a given orientation
	 * relative to the default Camera orientation.
	 * @param location		The location at which the created Camera will be set.
	 *						If <code>null</code>, the default location will be 
	 *						used.
	 * @param orientation	The orientation at which the created Camera will be 
	 *						set, relative to the default Camera orientation. If
	 *						<code>null</code>, the default orientation will be 
	 *						used.
	 */
	public Camera(Vector location, Rotation orientation) {
		// set location
		if (location != null)
			this.location = location;
		else
			this.location = Vector.ORIGIN;
		
		// set orientation
		if (orientation != null)
			this.orientation = orientation;
		else
			this.orientation = new Rotation(Quaternion.IDENTITY);
		
		// set default field values
		this.focusDepth = 1.0;
		this.focusLocked = false;
		this.fieldOfView = Math.toRadians(35);
		this.nearClip = 0.01;
		this.farClip = Double.MAX_VALUE;
	}
	
	/**
	 * Creates a Camera positioned at the origin (0, 0, 0) with the default Camera 
	 * orientation.
	 * @param orientation	The orientation at which the created Camera will be 
	 *						set, relative to the default Camera orientation.
	 */
	public Camera() {
		this(null, null);
	}
	
	
	/*********************************************
	 * MARK: Accessors
	 *********************************************/
	
	/**
	 * Returns this Camera's current location.
	 * @return	This Camera's current location.
	 */
	public Vector getLocation() {
		return this.location;
	}
	
	/**
	 * Returns this Camera's current orientation.
	 * @return	This Camera's current orientation.
	 */
	public Rotation getOrientation() {
		return this.orientation;
	}
	
	/**
	 * Returns this Camera's current focus depth.
	 * @return	This Camera's current focus depth.
	 */
	public double getFocusDepth() {
		return this.focusDepth;
	}
	
	/**
	 * Returns <code>true</code> if this Camera's focus is locked; <code>false</code>
	 * otherwise.
	 * @return	<code>true</code> if this Camera's focus is locked; <code>false</code>
	 *			otherwise.
	 */
	public boolean getFocusLocked() {
		return this.focusLocked;
	}
	
	/**
	 * Returns a vector pointing from this Camera's location to its focus point, 
	 *  in world coordinates. The focus vector is the same direction as the local 
	 * -z axis with magnitude equal to the Camera's focus depth.
	 * @return	This Camera's current focus vector.
	 */
	public Vector getFocusVector() {
		// focus vector has the same direction as the local -z axis:
		Vector focusVector = localZAxis().inverse();
		
		// set the magnitude to the current focus depth and return
		focusVector = new Vector(focusDepth, focusVector);
		return focusVector;
	}
	
	/**
	 * Returns this Camera's current point of focus.
	 * @return	This Camera's current point of focus.
	 */
	public Vector getFocusPoint() {
		// calculate and return the location pointed to by the focus vector
		return location.add(getFocusVector());
	}
	
	/**
	 * Returns this Camera's current field of view angle.
	 * @return	This Camera's current field of view angle.
	 */
	public double getFieldOfView() {
		return this.fieldOfView;
	}
	
	/**
	 * Returns the distance between this Camera and the near clipping plane.
	 * @return	The distance between this Camera and the near clipping plane.
	 */
	public double getNearClip() {
		return this.nearClip;
	}
	
	/**
	 * Returns the distance between this Camera and the far clipping plane.
	 * @return	The distance between this Camera and the far clipping plane.
	 */
	public double getFarClip() {
		return this.farClip;
	}

	/**
	 * Returns this Camera's local x axis in world coordinates.
	 * @return	This Camera's local x axis in world coordinates.
	 */
	public Vector localXAxis() {
		return Rotation.rotateVector(Vector.X_AXIS, this.orientation).unitVector();
	}
	
	/**
	 * Returns this Camera's local y axis in world coordinates.
	 * @return	This Camera's local y axis in world coordinates.
	 */
	public Vector localYAxis() {
		return Rotation.rotateVector(Vector.Y_AXIS, this.orientation).unitVector();
	}
	
	/**
	 * Returns this Camera's local z axis in world coordinates.
	 * @return	This Camera's local z axis in world coordinates.
	 */
	public Vector localZAxis() {
		return Rotation.rotateVector(Vector.Z_AXIS, this.orientation).unitVector();
	}
	
	
	/*********************************************
	 * MARK: Mutators
	 *********************************************/
	
	/**
	 * Sets this Camera's location to a given point.
	 * @param location	The location at which to set this Camera.
	 */
	public void setLocation(Vector location) {
		this.location = location;
	}
	
	/**
	 * Sets the Camera's orientation to a given Rotation, relative to the default
	 * camera orientation.
	 * @param orientation	The Rotation defining the desired orientation.
	 */
	public void setOrientation(Rotation orientation) {
		this.orientation = orientation;
	}
	
	/**
	 * Sets this Camera's focus depth to a given value. The given focus depth must
	 * be a positive value greater than <code>MIN_FOCUS_DEPTH</code>. If the given 
	 * value is not greater than <code>MIN_FOCUS_DEPTH</code>, then the focus 
	 * depth will be set to <code>MIN_FOCUS_DEPTH</code>.
	 * @param focusDepth	The distance from this Camera to its point of focus.
	 */
	public void setFocusDepth(double focusDepth) {
		this.focusDepth = (focusDepth > MIN_FOCUS_DEPTH)? focusDepth : MIN_FOCUS_DEPTH;
	}
	
	/**
	 * Sets whether this Camera's focus is locked. If the given value is 
	 * <code>true</code> then this Camera's focus will be locked. Otherwise, 
	 * it will be unlocked.
	 * <p>
	 * When a Camera's focus is locked, it will maintain its focus, regardless of
	 * any following mutation, unless its point of focus is explicitly changed 
	 * using the <code>lookAt</code> method.
	 * @param locked	A boolean value stating whether this Camera's focus should
	 *					be locked.
	 */
	public void setFocusLocked(boolean locked) {
		this.focusLocked = locked;
	}
	
	/**
	 * Sets this Camera's field of view to a given angle. The given angle will 
	 * be clamped to a value between 0 and 180 degrees.
	 * @param fov	The desired field of view angle in radians.
	 */
	public void setFieldOfView(double fov) {
		this.fieldOfView = fov;
	}
	
	/**
	 * Sets this camera's near clipping distance to a given value.
	 * @param nearClip	The distance between this Camera and the near clipping 
	 *					plane.
	 */
	public void setNearClip(double nearClip) {
		this.nearClip = nearClip;
	}
	
	/**
	 * Sets this camera's far clipping distance to a given value.
	 * @param farClip	The distance between this Camera and the far clipping 
	 *					plane.
	 */
	public void setFarClip(double farClip) {
		this.farClip = farClip;
	}
	
	
	/*********************************************
	 * MARK: Movement
	 *********************************************/
	
	/**
	 * Translates this Camera according to a given displacement vector. If the 
	 * <code>relative</code> flag is set to <code>true</code> then the translation 
	 * will be performed relative to this Camera's current orientation. Otherwise,
	 * the translation will be performed relative to the world coordinate system.
	 * @param displacement	A Vector defining the displacement to be applied to
	 *						this Camera.
	 * @param relative		A flag stating whether the translation performed 
	 *						should be relative to this Camera's current orientation.
	 */
	public void move(Vector displacement, boolean relative) {
		// find local and world displacement vectors 
		Vector worldDisp;
		Vector localDisp;
		if (relative) {
			worldDisp = Rotation.rotateVector(displacement, this.orientation);
			localDisp = displacement;
		} else {
			worldDisp = displacement;
			localDisp = Rotation.rotateVector(displacement, this.orientation.inverse());
		}
		
		// focus not locked?
		if (!focusLocked) {
			// move camera
			this.location = this.location.add(worldDisp);
			return;
		}
		
		// focus locked:
		
		// store z component of local displacement
		double zComp = localDisp.getZ();
		
		// create new displacement vector with only x and y components
		Vector rotationVector = new Vector(localDisp.getX(),
										   localDisp.getY(),
										   0);
		
		// find the angle of rotation 
		// (using rotation vector magnitude as arclength)
		double angle = rotationVector.magnitude() / focusDepth;
		
		// find the axis of rotation and normalize
		Vector axis = worldDisp.unitVector().cross(getFocusVector().unitVector());
		axis = axis.unitVector();
		
		// rotate camera around focus point
		rotate(new Rotation(axis, angle), getFocusPoint(), true);
		
		// clamp z component to a value that wont cause an illegal focus depth
		if ((focusDepth + zComp) < MIN_FOCUS_DEPTH)
			zComp = MIN_FOCUS_DEPTH - focusDepth;
		
		// calculate new focus depth
		setFocusDepth(focusDepth + zComp);
		
		// apply z component of displacement
		Vector zDisp = Rotation.rotateVector(new Vector(0, 0, zComp), this.orientation);
		this.location = this.location.add(zDisp);
	}
	
	/**
	 * Translates this Camera according to a given displacement, relative to the
	 * world coordinate system.
	 * <p>
	 * For example: applying the displacement vector (0, 0, -1) will move this 
	 * camera forward, applying the vector (0, 0, 1) will move this camera backward,
	 * applying the vector (1, 0, 0) will move this camera right, etc.
	 * <p>
	 * This method has the same effect as a call to 
	 * <code>move(displacement, true)</code>
	 * @param displacement	A Vector defining the displacement to be applied to
	 *						this Camera.
	 */
	public void move(Vector displacement) {
		move(displacement, true);
	}
	
	
	/*********************************************
	 * MARK: Rotation
	 *********************************************/
	
	/**
	 * Rotates this Camera by a given amount. If <code>relative</code> is set to
	 * <code>true</code> then the rotation will be performed relative to this 
	 * Camera's current orientation. Otherwise, the rotation will be performed
	 * relative to the world coordinate system.
	 * <p>
	 * If this Camera's focus is locked, then only rotation about the z axis (roll)
	 * will be performed.
	 * @param rotation	The amount of rotation to be applied to this Camera.
	 * @param relative	A flag stating whether the rotation performed should be 
	 *					relative to this Camera's current orientation.
	 */
	public void rotate(Rotation rotation, boolean relative) {
		// store current focus point
		Vector focusPoint = getFocusPoint();
		
		// perform rotation
		if (relative)
			this.orientation = this.orientation.append(rotation);
		else
			this.orientation = rotation.append(this.orientation);
		
		// if focus is locked, look at the point of focus
		if (focusLocked)
			lookAt(focusPoint);
	}
		
	
	/**
	 * Rotates this Camera by a given amount, relative to its current orientation.
	 * <p>
	 * If this Camera's focus is locked, then only rotation about the z axis (roll)
	 * will be performed.
	 * <p>
	 * This method has the same effect as a call to 
	 * <code>rotate(rotation, true)</code>
	 * @param rotation	The amount of rotation to be applied to this Camera.
	 */
	public void rotate(Rotation rotation) {
		rotate(rotation, true);
	}
	
	/**
	 * Rotates this Camera by a given amount around a given point. The rotation 
	 * will be performed relative to the world coordinate system. If the 
	 * <cod>reorient</code> is set to <code>true</code>, then the rotation will 
	 * be applied as a standard rotational transformation would; altering this 
	 * cameras orientation. If set to <code>false</code>, this camera's orientation
	 * will remain unchanged as the rotation is performed.
	 * @param rotation	The amount of rotation to be applied to this Camera.
	 * @param point		The point, in the world coordinate system, about which 
	 *					the rotation will occur.
	 * @param reorient	A flag stating whether or not to alter this camera's 
	 *					orientation as with the applied rotation.
	 */
	public void rotate(Rotation rotation, Vector point, boolean reorient) {
		// store current focus point
		Vector focusPoint = getFocusPoint();
		
		// find vector between given point and camera, in local coordinates
		Vector vect = this.location.subtract(point);
		
		// move camera to given point
		this.location = point;
		
		// rotate camera for standard rotation
		if (reorient)
			this.orientation = rotation.append(this.orientation);
		
		// rotate vector
		vect = Rotation.rotateVector(vect, rotation);
		
		// move back to proper position
		this.location = this.location.add(vect);
		
		// if focus is locked, look at original focus point
		if (focusLocked)
			lookAt(focusPoint);
	}
	
	/**
	 * Rotates this Camera to focus on a given point. The local y-axis (up vector)
	 * will be changed as little as possible.
	 * @param point	The point this Camera will focus on.
	 */
	public void lookAt(Vector point) {
		lookAt(point, localYAxis());
	}
	
	/**
	 * Rotates this Camera to focus on a given point with the local y-axis (up vector)
	 * set to a given vector. The up vector may be altered as needed to make it 
	 * perpendicular to the focus vector formed between this Camera's location
	 * and the given point.
	 * @param point	The point this Camera will focus on.
	 * @param up	A Vector defining the  desired local y axis (up vector).
	 */
	public void lookAt(Vector point, Vector up) {
		// find focus vector
		Vector focusVector = point.subtract(this.location);
		
		// set focus depth
		this.focusDepth = focusVector.magnitude();
		
		// set orientation (using inverse of focus vector for +z-axis
		this.orientation = new Rotation(up, focusVector.inverse());
	}
}

